前面的章節從 Collection 的基礎語法到核心程式碼都看了一輪,接下來想討論一下 Collection 可以如何活用?
我們之所以用 Collection,不止是因為它可以盛裝物件,或是它身上豐富的 API,而是因為 Collection 大多數的 method 都支援 Method Chain 的使用方式,讓各種操作之間還可以發揮綜效,也是 Kotlin 愛好者口中的「串串大法」。
比方說「a quick brown fox jump over a lazy dog」這句話是一個字串,我們可以先以空白做 split()
,接著馬上把回傳 List
裡小於 4 個字元的單字濾掉,然後把所有單字的字首變大寫,最後依照字母順序排序,取得 [Brown, Quick]
的結果。
"a quick brown fox jump over a lazy dog"
.split(" ")
.filter { it.length > 4 }
.map {
it.capitalize()
}
.sorted() // [Brown, Quick]
從上面這段範例應該可以看到 Collection 的迷人之處,每一個步驟的操作都很簡單易懂,但串在一起就是一個可以取代很多次 for
迴圈的複雜動作。
上一章討論 Extension 時,有提到一個 let()
函式,它是 Kotlin 標準函式庫裡被稱為 Scope Function 的函式,除了它以外,還有 run
、with
、apply
、also
及 takeIf
。這些函式很常與 Collection 一併使用,讓串串大法可以更好申連、程式碼更簡潔易讀。以下就逐一介紹這 6 個函式:
let()
函式let()
能使某個變數作用於其 Lambda,並回傳 Lambda 的結果值。我們思考以下這個情境,我們在 Collection 操作結束後想要把結果印出來,得先把結果放在一個變數後再印出來。
val numbers = mutableListOf("one", "two", "three", "four", "five")
val resultList = numbers.map { it.length }.filter { it > 3 }
println(resultList) // [5, 4, 4]
但有了 let()
函式後,可以省掉這個暫存變數,用串串大法完成。
val numbers = mutableListOf("one", "two", "three", "four", "five")
numbers.map { it.length }.filter { it > 3 }.let {
println(it) // [5, 4, 4]
// 還可以呼叫其他 function
}
以上兩段程式碼的結果是完全相同的,但使用 let()
的語法更簡潔,甚至我們還可以在 let()
裡面呼叫更多 function,程式的彈性更高了。
run()
函式run()
可以直接在物件身上呼叫,然後用 Lambda 處理 this
,最後回傳 Lambda 的結果值。下面的程式碼我們將 numbers 這個 MutableList 拿 run()
做了 2 次的 add()
及 1 次的 count()
,最後將結果存在 countEndsWithE 裡後輸出
val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run {
add("four")
add("five")
count { it.endsWith("e") }
}
println("There are $countEndsWithE elements that end with e.") // There are 3 elements that end with e.
with()
函式與 run()
不同,with()
則是要先傳入一個參數,然後拿 Lambda 處理這個參數,最後回傳 Lambda 的結果值。下面這個情境,我們可以把 MutableList
用 with()
對 this
套用一系列的動作,包括使用參數的屬性及方法。
val numbers = mutableListOf("one", "two", "three")
with(numbers) {
println("'with' is called with argument $this") // 'with' is called with argument [one, two, three]
println("It contains $size elements") // It contains 3 elements
}
val firstAndLast = with(numbers) {
"The first element is ${first()}, the last element is ${last()}"
}
println(firstAndLast) // The first element is one, the last element is three
apply()
函式跟 run()
有點類似,apply()
可以直接在物件身上呼叫,然後用 Lambda 處理 this
,但回傳的是物件本身。我們可以把這段程式碼唸成:在這個物件身上 apply 這些動作。也因此,範例裡的 apply()
的結果直接作用在 numberList 本身。
val numberList = mutableListOf(1.0)
numberList.apply {
add(2.71)
add(3.14)
add(5.0)
}
println(numberList) // [1.0, 2.71, 3.14, 5.0]
also()
函式also()
用起來和 let()
有點像,都是把物件傳給 Lambda,但 let()
回傳的是 Lambda 的結果,而 also()
則是回傳物件本身。
val numberList = mutableListOf<Double>()
numberList.apply {
add(2.71)
add(3.14)
add(1.0)
}
.also { println(it) } // [2.71, 3.14, 1.0]
takeIf()
及 takeUnless()
函式最後要看的是 takeIf()
,和以上 5 個有點不同,takeIf()
要判斷 Lambda 中提供的條件,若是 true 的話就回傳物件、若是 false 的話就回傳 null。takeIf()
想解決的是讓語法更貼英文、免除很多 if..else 的語句。
// 最簡單直覺的寫法
if (someObject != null && someObject.status) {
doThis()
}
// 套用了空安全檢查後的版本
if (someObject?.status == true) {
doThis()
}
// 使用 takeIf 後只要一句就可以完成
someObject?.takeIf{ it.status }?.apply{ doThis() }
而 takeUnless()
則是和 takeIf()
行為相同、但條件相反的版本。
一次學了 6 個動作和行為類似但又有些微差異的函式一定很苦惱,這邊一樣用表格幫大家整理如下:
物件參考 | 回傳值 | 是 Extension 嗎? | |
---|---|---|---|
let{} | it | Lambda 結果 | 是 |
run{} | this | Lambda 結果 | 是 |
run{} | - | Lambda 結果 | 否 |
with() | this | Lambda 結果 | 否 |
apply{} | this | Context 物件 | 是 |
also{} | is | Context 物件 | 是 |
takeIf{} | it | Context 物件 或 null | 是 |
takeUnless{} | it | Context 物件 或 null | 是 |
由於英文不是我們的母語,這幾個關鍵字可能會讓大家霧煞煞,這邊用 Julian Chu 的 這篇文章 裡的口訣來協助大家記憶:
Let it Run this literal, it Also Apply this
Let 用 it,Run 用 this,都是回傳 function literal 最後一行的值。Also 用 it,Apply 用 this,都是回傳 this。
花了這麼多篇幅就是要讓大家知道,Collection 加上 Scope Function 才是「串串大法」的精髓!